OC Runtime之Method Swizzling

什么是Method Swizzling

Method Swizzling,即方法交换,它通过在运行时重新映射方法(Selector)对应的实现(IMP)来达到“偷天换日”的目的。

Method Swizzling工作原理

通过改变类的调度表(dispatch table)中选择器到最终函数间的映射关系,Objectvie-C中方法的调用能够在运行时进行改变。

Method Swizzling的实现

用方法交换实现:当UIViewController或它子类的任何实例触发viewWillAppear:方法都会打印一条log日志。

#import <objc/runtime.h>

@implementation UIViewController (Tracking) 

+ (void)load { 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        Class class = [self class]; //self当前是类对象,[self class]还是类对象本身

        // 交换类方法时使用
        // Class class = object_getClass((id)self);//self当前是类对象,object_getClass((id)self)返回元类(meta class)


        SEL originalSelector = @selector(viewWillAppear:); 
        SEL swizzledSelector = @selector(xxx_viewWillAppear:); 

        Method originalMethod = class_getInstanceMethod(class, originalSelector); 
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); 
       //如果类中不存在要替换的方法,那就先用class_addMethod和class_replaceMethod函数添加和替换两个方法的实现;
        BOOL didAddMethod = 
            class_addMethod(class, 
                originalSelector, 
                method_getImplementation(swizzledMethod), 
                method_getTypeEncoding(swizzledMethod)); 

        if (didAddMethod) { 
            class_replaceMethod(class, 
                swizzledSelector, 
                method_getImplementation(originalMethod), 
                method_getTypeEncoding(originalMethod)); 
        } else { 
            method_exchangeImplementations(originalMethod, swizzledMethod); 
        } 
    }); 
} 

#pragma mark - Method Swizzling 

- (void)xxx_viewWillAppear:(BOOL)animated { 
    [self xxx_viewWillAppear:animated]; 
    NSLog(@"viewWillAppear: %@", self); 
} 

@end 
  1. +load vs. +initialize

    • + (void)load;

      The load message is sent to classes and categories that are both dynamically loaded and statically linked, but only if the newly loaded class or category implements a method that can respond.

      The order of initialization is as follows:

      All initializers in any framework you link to.

      All +load methods in your image.

      All C++ static initializers and C/C++ attribute(constructor) functions in your image.

      All initializers in frameworks that link to you.
      In addition:

      A class’s +load method is called after all of its superclasses’ +load methods. A category +load method is called after the class’s own +load method. In a custom implementation of load you can therefore safely message other unrelated classes from the same image, but any load methods implemented by those classes may not have run yet.

      • + (void)load; 在类文件被加载到 runtime 时被调用,即使没有被 #import 引用。调用顺序是 super class -> self class -> category。 每次 runtime 启动只会调用一次该方法。
      • + (void)load; 调用的时候尚未有自动释放池,所以如果 + (void)load; 中的内容使用到了自动释放池,则代码需要用 @autoreleasepool{} 包含。
      • 对应Category中的 + (void)load; 方法并不会覆盖类自身中的 + (void)load; 方法,两个 + (void)load; 方法都会调用,而且Category的 + (void)load;方法在类自身的 + (void)load;方法之后调用。
    • + (void)initialize;

      The runtime sends initialize to each class in a program just before the class, or any class that inherits from it, is sent its first message from within the program. The runtime sends the initialize message to classes in a thread-safe manner. Superclasses receive this message before their subclasses.The superclass implementation may be called multiple times if subclasses do not implement initialize—the runtime will call the inherited implementation—or if subclasses explicitly call [super initialize].

      • + (void)initialize; 在该类第一次发送消息(objc_msgSend(receiver, selector))时被调用。
      • 在应用程序的生命周期中,runtime 只会向每个类发送一次 + (void)initialize;消息,如果该类是子类,且该子类中没有实现 + (void)initialize;方法,或者子类显示调用父类实现 [super initialize], 那么则会调用其父类的实现。也就是说,父类的 + (void)initialize; 可能会被调用多次。如果不想子类使用父类的 + (void)initialize; 方法可以

        + (void)initialize {
            if (self == [ClassName self]) {
                // ... do the initialization ...
            }
        }
        
      • 如果类包含分类,且分类重写了 + (void)initialize; 方法,那么则会调用分类的 + (void)initialize; 实现,而原类的该方法实现不会被调用,这个机制同 NSObject 的其他方法(除 + (void)load 方法) 一样,即如果原类同该类的分类包含有相同的方法实现,那么原类的该方法被隐藏而无法被调用。

      • 父类的 + (void)initialize; 方法先于子类的 + (void)initialize; 方法调用。

  2. 选择器(typedef struct objc_selector *SEL):选择器用于表示一个方法在运行时的名字,一个方法的选择器是一个注册到(或映射到)Objective-C运行时中的C字符串,它是由编译器生成并在类加载的时候被运行时系统自动映射。

  3. 方法(typedef struct objc_method *Method):一个代表类定义中一个方法的不明类型。

  4. 实现(typedef id (*IMP)(id, SEL, …)):这种数据类型是实现某个方法的函数开始位置的指针,函数使用的是基于当前CPU架构的标准C调用规约。第一个参数是指向self的指针(也就是该类的某个实例的内存空间,或者对于类方法来说,是指向元类(metaclass)的指针)。第二个参数是方法的选择器,后面跟的都是参数。

    理解这些概念之间关系最好的方式是:一个类(Class)维护一张调度表(dispatch table)用于解析运行时发送的消息;调度表中的每个实体(entry)都是一个方法(Method),其中key值是一个唯一的名字——选择器(SEL),它对应到一个实现(IMP)——实际上就是指向标准C函数的指针。

    最后一段,xxx_viewWillAppear:方法的定义看似是递归调用引发死循环,其实不会的。因为[self xxx_viewWillAppear:animated]消息会动态找到xxx_viewWillAppear:方法的实现,而它的实现已经被我们与viewWillAppear:方法实现进行了互换,所以这段代码不仅不会死循环,如果你把[self xxx_viewWillAppear:animated]换成[self viewWillAppear:animated]反而会引发死循环。

Method Swizzling适用场景

Method Swizzling适用于向视图控制器的生命周期中注入操作、事件的响应、视图的绘制,或Foundation中的网络堆栈

Method Swizzling反作用

方法交换是全局的,容易导致不可预料的行为和结果,很难调试发现。曾经遇到同事在分类中交换掉UIViewController的dealloc方法,然而他在替换方法中只是输出当前类的名称,结果导致各种莫名crash(点击文本框都会崩)…,所以使用方法交换时,一定要始终调用方法的原始实现。另外,给swizzled方法加上前缀,可以避免冲突。

参考资料:

load&initialize

NSHipster原文:http://nshipster.com/method-swizzling/

中文翻译http://www.cocoachina.com/ios/20140225/7880.html

博客http://blog.csdn.net/yiyaaixuexi/article/details/9374411

JRSwizzle